
import * as obvm from '../runtime/vm.mjs'
import CodePointLib from '../runtime/codePoint.mjs'
import TextLib from '../runtime/text.mjs'
import * as obdebugger from '../runtime/debugger.mjs'
import * as VirtualServerLib from './virtualServerLib.mjs'
import LogView from '../../frontpage/components/LogView/LogView.mjs'
import * as Vue from 'vue';
import VirtualClient from './virtualClientLib.mjs';
import VRRFile from './VRRFile.mjs'
import * as oblib_sys from './syslib.js'
import DebugController from '../../frontpage/js/htmls/debugger/DebugSider.mjs';

let codepoint = new CodePointLib();
let text = new TextLib();
let virtualServers = new Map();
let ob_sys = new oblib_sys.Sys();
let nativeLibs = [
    ob_sys.install.bind(ob_sys),
    codepoint.install.bind(codepoint),
    text.install.bind(text),
    VirtualServerLib.default.install,
    VirtualClient.install
];



class ProxyFSM {
    sceneId;
    source; callerVMID; callerFsmID;
    constructor(sceneId, source, callerVMID, callerFsmID) {
        this.sceneId = sceneId;
        this.source = source;
        this.callerVMID = callerVMID;
        this.callerFsmID = callerFsmID;
        this.send = false;
    }
    toString() {
        return `ProxyFSM(${this.sceneId}, ${this.callerVMID}:${this.callerFsmID})`;
    }
    PostMessage(msg, st, uf, local) {
        if (!this.send) {
            this.send = true;
        } else {
            return;
        }
        let replyingVMID = msg.sender.VM.id;
        let replyingFsmID = msg.sender.id;
        msg = msg.sender.VM.messageToBuffer(msg, st, uf, local);
        let cmd = {
            cmd: 'virtualServerMessage_Reply',
            arg: {
                sceneId: this.sceneId,
                replyingVMID,
                replyingFsmID,
                targetVMID: this.callerVMID,
                targetFsmID: this.callerFsmID,
                msg
            }
        };
        this.source.postMessage(cmd, '*', [msg.buffer]);
    }
}

export class VirtualServer {
    /**
     * 检测长时间运行
     */
    frameStart;
    loadedScript;
    timeoutMap;
    sysLogLevel = 5;
    userLogLevel = 5;
    vm;
    vmdebugger;
    controller;
    constructor(scene, scriptArrayBuffer, controller) {
        this.scene = scene;
        this.controller = controller;
        this.scriptArrayBuffer = scriptArrayBuffer;
    }
    start() {
        let that = this;
        let vrrfile = new VRRFile(this.scene.id, this.update.bind(this));

        let _nativeLibs = [...nativeLibs, vrrfile.install.bind(vrrfile)];
        let isDebug = this.controller.options.debug;
        const timeout_ = isDebug ? 150000 : 15000;

        this.vmdebugger = isDebug ? new obdebugger.Debugger({
            breakpoints: this.controller.options.breakpoints,
            broker: new obdebugger.LogViewBroker(this.controller.ui.$refs.logview)
        }) : null;
        this.loadedScript = obvm.ScriptLoader.loadScript(this.scriptArrayBuffer, _nativeLibs, {
            debugger: this.vmdebugger,
            instructionWrap(instruction) {
                let pausable = obdebugger.Debugger.pausableInstructionWrap(instruction);
                return (st, func, local, i) => {
                    if (Date.now() - that.frameStart > timeout_) {
                        st.fsm.VM.pause();
                        throw new obvm.VMPausedException('func_timeout');
                    }
                    return pausable.apply(null, [st, func, local, i]);
                };
            }
        });
        this.timeoutMap = new Map();
        this.vm = new obvm.VM(this.loadedScript, {
            debugger: this.vmdebugger,
            setTimeout: setTimeout.bind(window), Output: this.debugLog.bind(this),
        });
        this.vm.__scene = this.scene;
        this.vm.usrLogLevel = this.userLogLevel;
        this.vm.sysLogLevel = this.sysLogLevel;
        let fsmname = this.loadedScript.entryFSMName;
        let fsm = this.vm.CreateFSM(fsmname);
        if (!fsm) {
            this.debugLog("No FSM named " + fsmname);
        } else {
            this.update();
        }
        this.controller.ui.running = this.vm.isRunning();
    }
    stop() {
        if (this.timeoutMap) {
            this.timeoutMap.forEach((value, key) => {
                clearTimeout(value);
            });
            this.timeoutMap = null;
        }
        this.vm.Destroy();
        this.vm = null;
        this.controller.ui.running = false;
    }
    update() {
        let vm = this.vm;
        if (vm) {
            this.frameStart = Date.now();
            try {
                vm.update();
                setTimeout(() => {
                    this.update();
                }, 500);
            } catch (e) {
                console.error(e);
                this.debugLog(e.message, 'sys', 8);
            }
        }
    }
    debugLog(msg, type, level, stackpath, block) {
        if ((type == 'usr' && level >= this.userLogLevel)
            || level >= this.sysLogLevel) {
            if (this.controller) {
                this.controller.ui.log(msg, type, level, stackpath, block);
            } else {
                console.log({ type, level, msg, block, stackpath });
            }
        }
    }
    setLogLevel(userLogLevel, sysLogLevel) {
        this.userLogLevel = userLogLevel;
        this.sysLogLevel = sysLogLevel;
        if (this.vm) {
            this.vm.usrLogLevel = this.userLogLevel;
            this.vm.sysLogLevel = this.sysLogLevel;
        }
    }
}

let id = 0;
let UI = Vue.defineAsyncComponent(async () => {
    await OpenBlock.onInitedPromise();
    return {
        name: "VirtualServerUI",
        components: { LogView },
        template: (await axios.get('/openblock/jsruntime/server/VirtualServer.html')).data,
        data() {
            return {
                enabled: true,
                scene: null,
                miniMode: true,
                controller: null,
                running: false,
                id: id++
            };
        },
        watch: {
            enabled: {
                handler(newV, oldV) {
                    if (!newV) {
                        OB_IDE.removeComponent(this);
                        this.stop();
                        this.controller.dispatchEvent(new CustomEvent('closed'));
                    }
                }
            },
            controller: {
                handler() {
                    this.running = this.controller?.server?.vm?.isRunning();
                    this.miniMode = !this.controller?.options?.debug;
                }
            }
        },

        mounted() {
            DebugController.registerBreakpointListener('VirtualServer-' + this.id, (eventName, breakpoint) => {
                this.controller.server?.vmdebugger?.broker?.onMessage({ 'cmd': 'debug', method: eventName + 'Breakpoint', args: [(DebugController.breakpointKey(breakpoint))] });
            });
        },
        beforeUnmount() {
            DebugController.unregisterBreakpointListener('VirtualServer-' + this.id);
        },
        methods: {
            sendMessage(msg) {
                this.controller.server?.vmdebugger?.broker?.onMessage(msg);
                this.running = this.controller?.server?.vm?.isRunning();
            },
            onLogLevelChange(arg) {
                this.controller.updateLogLevel(arg);
            },
            stop() {
                this.controller.stop();
            },
            start() {
                this.controller.start();
            },
            log(msg, type, level, stackpath, block) {
                let v = { type, level, msg, block, stackpath };
                this.$refs['logview'].putLog(v);
            }
        },
    };
});
export class VirtualServerUI extends EventTarget {
    server;
    userLogLevel = 5;
    sysLogLevel = 5;
    ui;
    constructor(scene, scriptArrayBuffer, options) {
        super();
        this.scene = scene;
        this.options = options;
        this.scriptArrayBuffer = scriptArrayBuffer;
        OB_IDE.addComponent(UI).then(ui => {
            this.ui = ui;
            ui.controller = (this);
            ui.scene = scene;
        });
    }
    start(f) {
        if ((!f) || (!this.ui)) {
            setTimeout(() => this.start(true), 200);
            return;
        }
        this.stop();
        this.server = new VirtualServer(this.scene, this.scriptArrayBuffer, this);
        this.server.start();
        this.dispatchEvent(new CustomEvent('ServerStart', {
            detail: {}
        }));
    }
    stop() {
        if (this.server) {
            this.server.stop();
            this.dispatchEvent(new CustomEvent('ServerStop', {
                detail: {}
            }));
            this.server = null;
        }
    }
    updateLogLevel(arg) {
        this.userLogLevel = arg.userLogLevel;
        this.sysLogLevel = arg.sysLogLevel;
        if (this.server) {
            this.server.setLogLevel(this.userLogLevel, this.sysLogLevel);
        }

    }
}

let messageHandlers = {
    virtualServerMessage(evt) {
        let targetServer = evt.data.arg.target;
        let callerFsmID = evt.data.arg.callerFsmID;
        let callerVMID = evt.data.arg.callerVMID;
        let serverUI = virtualServers.get(targetServer);
        if (!(serverUI && serverUI.server)) {
            evt.source.postMessage(
                {
                    cmd: 'virtualServerMessage_NetworkError',
                    arg: {
                        target: targetServer,
                        targetVMID: callerVMID,
                        targetFsmID: callerFsmID,
                        type: 'error',
                        msg: `fetch failed`
                    }
                },
                '*'
            );
            return;
        }
        let vServer = serverUI.server;
        let vm = vServer.vm;
        let targetFsmID = evt.data.arg.targetFsmID;
        if (targetFsmID) {
            let targetVMID = evt.data.arg.targetVMID;
            if (vm.id != targetVMID) {
                return;
            }
            let fsm = vm.getFsmByID(targetFsmID);
            if (!fsm) {
                return;
            }
            let pfsm = new ProxyFSM(vServer.scene.id, evt.source, callerVMID, callerFsmID);
            let msg = vm.u8arrayToMessage(new Uint8Array(evt.data.arg.msg), () => {
                return pfsm;
            });
            msg.__virtual_pfsm = pfsm;
            fsm.PostMessage(msg);
        } else if (vm.__virtualServerPortal) {
            try {
                let pfsm = new ProxyFSM(vServer.scene.id, evt.source, callerVMID, callerFsmID);
                let msg = vm.u8arrayToMessage(new Uint8Array(evt.data.arg.msg), () => {
                    return pfsm;
                });
                msg.__virtual_pfsm = pfsm;
                vm.__virtualServerPortal.PostMessage(msg);
            } catch (e) {
                console.error(e);
                evt.source.postMessage(
                    {
                        cmd: 'virtualServerMessage_ServiceError',
                        arg: {
                            target: targetServer,
                            targetVMID: callerVMID,
                            targetFsmID: callerFsmID,
                            type: 'error',
                            msg: `${e.message}`
                        }
                    },
                    '*'
                );
                return;
            }
        }
    }
}

window.addEventListener('message', (evt) => {
    let data = evt.data;
    let cmd = data && data.cmd;
    if (cmd && messageHandlers[cmd]) {
        messageHandlers[cmd](evt);
    }
});
export function startServerVM(scene, scriptArrayBuffer, options) {

    let virtualServer = virtualServers.get(scene.id);
    if (virtualServer) {
        virtualServer.stop();
        virtualServer.scene = scene;
        virtualServer.options = options || {};
        virtualServer.scriptArrayBuffer = scriptArrayBuffer;
    } else {
        virtualServer = new VirtualServerUI(scene, scriptArrayBuffer, options || {});
        virtualServer.addEventListener('closed', () => {
            virtualServers.delete(scene.id);
        });
        virtualServers.set(scene.id, virtualServer);
    }
    virtualServer.start();
}
